今天要來介紹不管在哪個語言都是非常常用的資料結構 List ,在 Haskell 中 List 中每個元素只能是同樣型別
-- ⭕️
[1,2,3,4,5,6]
['a','b','c']
-- ❌
[1,2,'c',4,'a',6]
[1,'b','c']
接下來讓我們來看 Haskell 裡 List 的各種操作吧
在 Haskell 中 List 的宣告方式為
-- in ghci
x = [1,2,3,4,5,6,7]
x -- [1,2,3,4,5,6,7]
有一個比較重要的概念是
在 Haskell 中 String
與 [Char]
是相等的
也就是說"abc"
以及 ['a','b','c']
是一樣的東西
['a','b','c'] == "abc"
如果我們今天要串接 List 我們可以使用 ++
[1,2,3,4] ++ [5,6] -- [1,2,3,4,5,6]
['a','b'] ++ ['c'] -- "abc"
"ab" ++ "c" -- "abc"
在使用 ++
要特別注意的是他是會將左邊運算元遍歷過一次,所以如果左邊 是一個很長的 List 的話必須警慎使用。
那如果我們是要在 List 前面塞入一個元素可以使用 :
特別注意一下這裡是 'a'
而不是 "a"
差異為一個是 Char
與 [Char]
(String
) 。
'a' : "bc" -- "abc"
1 : [2,3,4] -- [1,2,3,4]
我們也可以一直使用 :
來組成一個 List
'f' : 'o' : 'o' : [] -- "foo"
0 : 1 : 2 : [] -- [0,1,2]
其實 Haskell 在建立 List 時就是不斷遞迴用 :
把左邊運算子不斷的塞入來建立出 List 的,所以[0,1,2]
實際上就是 0 : 1 : 2 : []
的語法糖。
那如果我們要存取特定的元素的話我們能用 !!
[1,2,3,4,5,6] !! 0 -- 1
"abc" !! 0 -- 'a'
"abc" !! 1 -- 'b'
x = [1,2,3,4,5,6]
head x -- 1
tail x -- [2,3,4,5,6]
last x -- 6
init x -- [1,2,3,4,5]
head
會回傳 List 中第一個元素
tail
會回傳扣除第一個元素後的List
last
會回傳 List 的最後一個元素
init
會回傳扣除最後一個元素後的List
有這些我們就能夠來寫出一個簡單小 function 來操作 List
listSum :: [Int] -> Int
listSum x = if null x then 0 else head x + listSum (tail x)
main :: IO ()
main = do
let x = [1,2,3,4,5,6,7,8,9,10]
putStrLn (show(listSum x))
這裡我們使用了 head
及 tail
協助我們加總 List 中的所有元素,listSum
解釋起來就是當 x
不為 null 時就會將 x
的第一個元素與listSum (tail x)
相加。
沒錯就是使用遞迴,因為在 Haskell 中我們所有 variable 都是 immutable 的所以我們寫不出像 js 這樣的程式碼
a = 0
list = [1,2,3,4,5,6,7,8,9,10]
list.forEach(x => a += x)
大家先有一個概念就是 Haskell 上的在定義變數時 =
可以用數學上的「等於」來解釋,在數學上我們不可能說有一個「數值 x」是等於 x + 1 吧。
像是我先定義了 x = 5,不可能又有一個等式說明了 x = 6,所以也不會發生 x = x + 1 也就是5 = 6 了這件事情了。
當然,如果是描述函式關係的話這樣 x = x + 1 是非常合理的描述
除了上面四個幫我們快速找到頭尾的function 以外 Haskell 還有提供其他好用的 funtion
length [1,2,3,4,5] -- 5
reverse [1,2,3,4,5] -- [5,4,3,2,1]
take 2 [1,2,3,4,5] -- [1,2]
maximum [1,2,3,4,5] -- 5
minimum [1,2,3,4,5] -- 1
product [1,2,3,4,5] -- 120
sum [1,2,3,4,5] -- 15
elem 4 [1,2,3,4,5] -- True
length
會回傳 List 長度
take
會根據根據傳入的長度及List 回傳一個新的 List
reverse
會回傳一個反轉的 List
maximum
回傳 List 中最大的數
minimum
回傳 List 中最小的數
sum
List所有元素的加總
product
List所有元素的乘積
elem
判斷該元素是否屬於該List
通常我們都是使用
4
elem[1,2,3,4,5]
這種 infix 的形式在使用elem
當然我們要建立一個 List
不一定每次都要直接寫出所有的元素
x = [1,2,3,4]
我們可使用 ..
來幫我快速建立一個 List
[1..10] -- [1,2,3,4,5,6,7,8,9,10]
['a'..'z'] -- "abcdefghijklmnopqrstuvwxyz"
這種方式稱為 range ,而能夠使用 range 來建立 List 那個值本身必須是可以被枚舉的。像是A到Z,1到10。
那我們也能限制 range 所產生的元素之間的距離,只要多放一個元素且標註上限就好
[2,4..20] -- [2,4,6,8,10,12,14,16,18,20]
[5,10..49] -- [5,10,15,20,25,30,35,40,45]
那如果我們不限制 range 上限呢?那我們就可以產生出一個無限的 List 。
[1..] -- 這會產生一個無限 List 如果要在ghci停止請 使用 command+c 來停止
所以假設我們今天想產生前17個17的倍數,我們除了直接標註上限是17*100以外,我們也可以利用 take
加上無限 List 來幫助我們達成這個需求
take 17 [17,34..]
-- [17,34,51,68,85,102,119,136,153,170,187,204,221,238,255,272,289]
除了使用 range 我們也可以用 cycle
以及 repeat
來產生無限 List
take 5 (cycle "abc") -- "abcab"
take 5 (repeat 'a') -- "aaaaa"
-- 上面的 repeat 的寫法可以使用 replicate 簡化
replicate 5 'a' -- "aaaaa"
今天的程式碼一樣會放到 github
補充
所謂的partial function就是傳入某些合法的值卻回傳bottom的function。合法的值指這個function定義的input type的值域,例如Int裡就只有數字;bottom就是會讓程式壞掉的值,例如拋出exception。
Haskell預設import的function裡就包含著這些function,例如head
、tail
、init
,只要傳empty list進去程式就會壞掉,而Haskell在處理exception的議題上非常非常棘手,原因就是它沒有stack trace,你絕對不會希望在production上程式丟出exception然後查不出原因(Haskell的stack trace有三種不同的產生方式,但沒有一個完美的做法,要嘛可讀性差,要嘛會影響效能,要嘛stack trace中間會斷掉)。
相關的partial function可以參考haskell-dangerous-functions。
Haskell一家獨大list,什麼東西都用list表示,唯一內建的資料結構只有list,但它是linked list呀!還是單向的!它的insert和index操作都是O(n),你呼叫一次length
也是O(n),它可不會把list長度cache起來。String用a list of characters表示也實在不是明智的做法,試想一下在Java裡你會用LinkedList<Character>來表示字串嗎?不會呀!Character被box過,要間接由pointer去存取,這在Haskell也是,不僅佔空間又影響效能。
所以請考慮一下使用別的資料結構,以下為幾乎被認為是標準的幾個函式庫(Haskell裡實在是不存在真正標準的函式庫,內建的唯一函式庫base功能是真的少):
上面的資料結構通常又有分lazy和strict的版本,我個人是認為沒什麼特別需求就都選strict的版本,lazy會堆一些尚未evaluated的程式區塊,我們稱作thunk,這東西是造成memory space leak的元兇。
這裡附上幾個資料結構的benchmark,可以參考這個來選資料結構:
https://github.com/haskell-perf/sequences
https://github.com/haskell-perf/dictionaries
非常感謝大大的補充
也許是我並沒有真正拿 Haskell 開發過所以其實我並沒有認真看待過效能及例外處理的問題。
那我是不是能理解 Haskell 的 String
算是一種設計錯誤?像是其他語言中可能是分配一塊記憶體位置來作存放
Haskell的String
我想大多數人都會認為它是設計錯誤,String
除了可以直接拿List的function來用之外,也沒其他好處了。
Haskell預設import的module Prelude
基本上也是設計錯誤,裡面的partial function應該要全部拿掉才是,於是就有一大堆的函式庫弄了自己的Prelude
版本想取而代之,其中也有把String
全部換成Text
的例子。
不過一改就是breaking change,一堆函式庫都會壞掉的。